package alerting

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"testing"
	"time"

	"github.com/go-openapi/strfmt"
	amv2 "github.com/prometheus/alertmanager/api/v2/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/grafana/grafana/pkg/apimachinery/errutil"
	apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
	"github.com/grafana/grafana/pkg/services/org"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/tests/testinfra"
	"github.com/grafana/grafana/pkg/util"
)

func TestIntegrationAMConfigAccess(t *testing.T) {
	testinfra.SQLiteIntegrationTest(t)

	dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
		DisableLegacyAlerting: true,
		EnableUnifiedAlerting: true,
		DisableAnonymous:      true,
		AppModeProduction:     true,
	})

	grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)

	// Create a users to make authenticated requests
	createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
		DefaultOrgRole: string(org.RoleViewer),
		Password:       "viewer",
		Login:          "viewer",
	})
	createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
		DefaultOrgRole: string(org.RoleEditor),
		Password:       "editor",
		Login:          "editor",
	})
	createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
		DefaultOrgRole: string(org.RoleAdmin),
		Password:       "admin",
		Login:          "admin",
	})

	type testCase struct {
		desc      string
		url       string
		expStatus int
		expBody   string
	}

	// Create alertmanager config
	cfg := apimodels.PostableUserConfig{}
	amConfig := `
		{
			"alertmanager_config": {
				"route": {
					"receiver": "grafana-default-email"
				},
				"receivers": [{
					"name": "grafana-default-email",
					"grafana_managed_receiver_configs": [{
						"uid": "",
						"name": "email receiver",
						"type": "email",
						"isDefault": true,
						"settings": {
							"addresses": "<example@email.com>"
						}
					}]
				}]
			}
		}
		`
	err := json.Unmarshal([]byte(amConfig), &cfg)
	require.NoError(t, err)
	err = env.Server.HTTPServer.AlertNG.MultiOrgAlertmanager.SaveAndApplyAlertmanagerConfiguration(context.Background(), 1, cfg)
	require.NoError(t, err)

	t.Run("when retrieve alertmanager configuration", func(t *testing.T) {
		cfgTemplate := `
		{
			"template_files": null,
			"alertmanager_config": {
				"route": %s,
				"receivers": [{
					"name": "grafana-default-email",
					"grafana_managed_receiver_configs": [{
						"disableResolveMessage": false,
						"uid": "",
						"name": "email receiver",
						"type": "email",
						"secureFields": {},
						"settings": {
							"addresses": "<example@email.com>"
						}
					}]
				}]
			}
		}
		`
		cfgWithoutAutogen := fmt.Sprintf(cfgTemplate, `{
			"receiver": "grafana-default-email"
		}`)
		cfgWithAutogen := fmt.Sprintf(cfgTemplate, `{
					"receiver": "grafana-default-email",
					"routes": [{
						"receiver": "grafana-default-email",
						"object_matchers": [["__grafana_autogenerated__", "=", "true"]],
						"routes": [{
							"receiver": "grafana-default-email",
							"group_by": ["grafana_folder", "alertname"],
							"object_matchers": [["__grafana_receiver__", "=", "grafana-default-email"]]
						}]
					}]
				}`)

		testCases := []testCase{
			{
				desc:      "un-authenticated request should fail",
				url:       "http://%s/api/alertmanager/grafana/config/api/v1/alerts",
				expStatus: http.StatusUnauthorized,
				expBody:   `{"extra":null,"message":"Unauthorized","messageId":"auth.unauthorized","statusCode":401,"traceID":""}`,
			},
			{
				desc:      "viewer request should succeed",
				url:       "http://viewer:viewer@%s/api/alertmanager/grafana/config/api/v1/alerts",
				expStatus: http.StatusOK,
				expBody:   cfgWithoutAutogen,
			},
			{
				desc:      "editor request should succeed",
				url:       "http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts",
				expStatus: http.StatusOK,
				expBody:   cfgWithoutAutogen,
			},
			{
				desc:      "admin request should succeed",
				url:       "http://admin:admin@%s/api/alertmanager/grafana/config/api/v1/alerts",
				expStatus: http.StatusOK,
				expBody:   cfgWithAutogen,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.desc, func(t *testing.T) {
				resp, err := http.Get(fmt.Sprintf(tc.url, grafanaListedAddr))
				t.Cleanup(func() {
					require.NoError(t, resp.Body.Close())
				})
				require.NoError(t, err)
				require.Equal(t, tc.expStatus, resp.StatusCode)
				b, err := io.ReadAll(resp.Body)
				if tc.expStatus == http.StatusOK {
					re := regexp.MustCompile(`"uid":"([\w|-]+)"`)
					b = re.ReplaceAll(b, []byte(`"uid":""`))
				}
				require.NoError(t, err)
				require.JSONEq(t, tc.expBody, string(b))
			})
		}
	})

	t.Run("when creating silence", func(t *testing.T) {
		now := time.Now()
		body := fmt.Sprintf(`
		{
			"comment": "string",
			"createdBy": "string",
			"matchers": [
			  {
				"isRegex": true,
				"name": "string",
				"value": "string"
			  }
			],
			"startsAt": "%s",
			"endsAt": "%s"
		  }
		`, now.Format(time.RFC3339), now.Add(10*time.Second).Format(time.RFC3339))

		testCases := []testCase{
			{
				desc:      "un-authenticated request should fail",
				url:       "http://%s/api/alertmanager/grafana/config/api/v2/silences",
				expStatus: http.StatusUnauthorized,
				expBody:   `"message":"Unauthorized"`,
			},
			{
				desc:      "viewer request should fail",
				url:       "http://viewer:viewer@%s/api/alertmanager/grafana/api/v2/silences",
				expStatus: http.StatusForbidden,
				expBody:   `"title":"Access denied"`,
			},
			{
				desc:      "editor request should succeed",
				url:       "http://editor:editor@%s/api/alertmanager/grafana/api/v2/silences",
				expStatus: http.StatusAccepted,
			},
			{
				desc:      "admin request should succeed",
				url:       "http://admin:admin@%s/api/alertmanager/grafana/api/v2/silences",
				expStatus: http.StatusAccepted,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.desc, func(t *testing.T) {
				url := fmt.Sprintf(tc.url, grafanaListedAddr)
				buf := bytes.NewReader([]byte(body))
				// nolint:gosec
				resp, err := http.Post(url, "application/json", buf)
				t.Cleanup(func() {
					require.NoError(t, resp.Body.Close())
				})
				require.NoError(t, err)
				require.Equal(t, tc.expStatus, resp.StatusCode)
				b, err := io.ReadAll(resp.Body)
				require.NoError(t, err)
				if tc.expStatus == http.StatusAccepted {
					response := apimodels.PostSilencesOKBody{}
					require.NoError(t, json.Unmarshal(b, &response))
					require.NotEmpty(t, response.SilenceID)
					return
				}
				require.Contains(t, string(b), tc.expBody)
			})
		}
	})

	var blob []byte
	t.Run("when getting silences", func(t *testing.T) {
		testCases := []testCase{
			{
				desc:      "un-authenticated request should fail",
				url:       "http://%s/api/alertmanager/grafana/api/v2/silences",
				expStatus: http.StatusUnauthorized,
				expBody:   `"message": "Unauthorized"`,
			},
			{
				desc:      "viewer request should succeed",
				url:       "http://viewer:viewer@%s/api/alertmanager/grafana/api/v2/silences",
				expStatus: http.StatusOK,
			},
			{
				desc:      "editor request should succeed",
				url:       "http://editor:editor@%s/api/alertmanager/grafana/api/v2/silences",
				expStatus: http.StatusOK,
			},
			{
				desc:      "admin request should succeed",
				url:       "http://admin:admin@%s/api/alertmanager/grafana/api/v2/silences",
				expStatus: http.StatusOK,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.desc, func(t *testing.T) {
				url := fmt.Sprintf(tc.url, grafanaListedAddr)
				// nolint:gosec
				resp, err := http.Get(url)
				t.Cleanup(func() {
					require.NoError(t, resp.Body.Close())
				})
				require.NoError(t, err)
				require.Equal(t, tc.expStatus, resp.StatusCode)
				require.NoError(t, err)
				if tc.expStatus == http.StatusOK {
					b, err := io.ReadAll(resp.Body)
					require.NoError(t, err)
					blob = b
				}
			})
		}
	})

	var silences apimodels.GettableSilences
	err = json.Unmarshal(blob, &silences)
	require.NoError(t, err)
	assert.Len(t, silences, 2)
	silenceIDs := make([]string, 0, len(silences))
	for _, s := range silences {
		silenceIDs = append(silenceIDs, *s.ID)
	}

	unconsumedSilenceIdx := 0
	t.Run("when deleting a silence", func(t *testing.T) {
		testCases := []testCase{
			{
				desc:      "un-authenticated request should fail",
				url:       "http://%s/api/alertmanager/grafana/api/v2/silence/%s",
				expStatus: http.StatusUnauthorized,
				expBody:   `"message":"Unauthorized"`,
			},
			{
				desc:      "viewer request should fail",
				url:       "http://viewer:viewer@%s/api/alertmanager/grafana/api/v2/silence/%s",
				expStatus: http.StatusForbidden,
				expBody:   `"title":"Access denied"`,
			},
			{
				desc:      "editor request should succeed",
				url:       "http://editor:editor@%s/api/alertmanager/grafana/api/v2/silence/%s",
				expStatus: http.StatusOK,
				expBody:   `{"message":"silence deleted"}`,
			},
			{
				desc:      "admin request should succeed",
				url:       "http://admin:admin@%s/api/alertmanager/grafana/api/v2/silence/%s",
				expStatus: http.StatusOK,
				expBody:   `{"message":"silence deleted"}`,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.desc, func(t *testing.T) {
				url := fmt.Sprintf(tc.url, grafanaListedAddr, silenceIDs[unconsumedSilenceIdx])

				// Create client
				client := &http.Client{}

				// Create request
				req, err := http.NewRequest("DELETE", url, nil)
				if err != nil {
					fmt.Println(err)
					return
				}

				// Fetch Request
				resp, err := client.Do(req)
				if err != nil {
					return
				}
				t.Cleanup(func() {
					require.NoError(t, resp.Body.Close())
				})
				require.NoError(t, err)
				require.Equal(t, tc.expStatus, resp.StatusCode)
				b, err := io.ReadAll(resp.Body)
				require.NoError(t, err)
				if tc.expStatus == http.StatusOK {
					unconsumedSilenceIdx++
				}
				require.Contains(t, string(b), tc.expBody)
			})
		}
	})
}

func TestIntegrationAlertmanagerCreateSilence(t *testing.T) {
	testinfra.SQLiteIntegrationTest(t)
	dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
		DisableLegacyAlerting: true,
		EnableUnifiedAlerting: true,
		AppModeProduction:     true,
	})
	grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
	createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
		DefaultOrgRole: string(org.RoleAdmin),
		Password:       "admin",
		Login:          "admin",
	})
	client := newAlertingApiClient(grafanaListedAddr, "admin", "admin")

	cases := []struct {
		name    string
		silence apimodels.PostableSilence
		expErr  string
	}{{
		name: "can create silence for foo=bar",
		silence: apimodels.PostableSilence{
			Silence: amv2.Silence{
				Comment:   util.Pointer("This is a comment"),
				CreatedBy: util.Pointer("test"),
				EndsAt:    util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
				Matchers: amv2.Matchers{{
					IsEqual: util.Pointer(true),
					IsRegex: util.Pointer(false),
					Name:    util.Pointer("foo"),
					Value:   util.Pointer("bar"),
				}},
				StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
			},
		},
	}, {
		name: "can create silence for _foo1=bar",
		silence: apimodels.PostableSilence{
			Silence: amv2.Silence{
				Comment:   util.Pointer("This is a comment"),
				CreatedBy: util.Pointer("test"),
				EndsAt:    util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
				Matchers: amv2.Matchers{{
					IsEqual: util.Pointer(true),
					IsRegex: util.Pointer(false),
					Name:    util.Pointer("_foo1"),
					Value:   util.Pointer("bar"),
				}},
				StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
			},
		},
	}, {
		name: "can create silence for 0foo=bar",
		silence: apimodels.PostableSilence{
			Silence: amv2.Silence{
				Comment:   util.Pointer("This is a comment"),
				CreatedBy: util.Pointer("test"),
				EndsAt:    util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
				Matchers: amv2.Matchers{{
					IsEqual: util.Pointer(true),
					IsRegex: util.Pointer(false),
					Name:    util.Pointer("0foo"),
					Value:   util.Pointer("bar"),
				}},
				StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
			},
		},
	}, {
		name: "can create silence for foo=🙂bar",
		silence: apimodels.PostableSilence{
			Silence: amv2.Silence{
				Comment:   util.Pointer("This is a comment"),
				CreatedBy: util.Pointer("test"),
				EndsAt:    util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
				Matchers: amv2.Matchers{{
					IsEqual: util.Pointer(true),
					IsRegex: util.Pointer(false),
					Name:    util.Pointer("foo"),
					Value:   util.Pointer("🙂bar"),
				}},
				StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
			},
		},
	}, {
		name: "can create silence for foo🙂=bar",
		silence: apimodels.PostableSilence{
			Silence: amv2.Silence{
				Comment:   util.Pointer("This is a comment"),
				CreatedBy: util.Pointer("test"),
				EndsAt:    util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
				Matchers: amv2.Matchers{{
					IsEqual: util.Pointer(true),
					IsRegex: util.Pointer(false),
					Name:    util.Pointer("foo🙂"),
					Value:   util.Pointer("bar"),
				}},
				StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
			},
		},
	}, {
		name: "can't create silence for missing label name",
		silence: apimodels.PostableSilence{
			Silence: amv2.Silence{
				Comment:   util.Pointer("This is a comment"),
				CreatedBy: util.Pointer("test"),
				EndsAt:    util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
				Matchers: amv2.Matchers{{
					IsEqual: util.Pointer(true),
					IsRegex: util.Pointer(false),
					Name:    util.Pointer(""),
					Value:   util.Pointer("bar"),
				}},
				StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
			},
		},
		expErr: "unable to upsert silence: invalid silence: invalid label matcher 0: invalid label name \"\": unable to create silence",
	}, {
		name: "can't create silence for missing label value",
		silence: apimodels.PostableSilence{
			Silence: amv2.Silence{
				Comment:   util.Pointer("This is a comment"),
				CreatedBy: util.Pointer("test"),
				EndsAt:    util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
				Matchers: amv2.Matchers{{
					IsEqual: util.Pointer(true),
					IsRegex: util.Pointer(false),
					Name:    util.Pointer("foo"),
					Value:   util.Pointer(""),
				}},
				StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
			},
		},
		expErr: "unable to upsert silence: invalid silence: at least one matcher must not match the empty string: unable to create silence",
	}}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			silenceOkBody, status, body := client.PostSilence(t, tc.silence)
			t.Log(body)
			if tc.expErr != "" {
				assert.NotEqual(t, http.StatusAccepted, status)

				var validationError errutil.PublicError
				assert.NoError(t, json.Unmarshal([]byte(body), &validationError))
				assert.Contains(t, validationError.Message, tc.expErr)
				assert.Empty(t, silenceOkBody.SilenceID)
			} else {
				assert.Equal(t, http.StatusAccepted, status)
				assert.NotEmpty(t, silenceOkBody.SilenceID)
			}
		})
	}
}

func TestIntegrationAlertmanagerStatus(t *testing.T) {
	testinfra.SQLiteIntegrationTest(t)

	// Setup Grafana and its Database
	dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
		DisableLegacyAlerting: true,
		EnableUnifiedAlerting: true,
		DisableAnonymous:      true,
		AppModeProduction:     true,
	})

	grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
	// Create a users to make authenticated requests
	createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
		DefaultOrgRole: string(org.RoleViewer),
		Password:       "viewer",
		Login:          "viewer",
	})
	createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
		DefaultOrgRole: string(org.RoleEditor),
		Password:       "editor",
		Login:          "editor",
	})
	createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
		DefaultOrgRole: string(org.RoleAdmin),
		Password:       "admin",
		Login:          "admin",
	})

	type testCase struct {
		desc      string
		url       string
		expStatus int
		expBody   string
	}

	cfgTemplate := `
{
	"cluster": {
		"peers": [],
		"status": "disabled"
	},
	"config": {
		"route": %s,
		"receivers": [{
			"name": "grafana-default-email",
			"grafana_managed_receiver_configs": [{
				"uid": "",
				"name": "email receiver",
				"type": "email",
				"disableResolveMessage": false,
				"settings": {
					"addresses": "\u003cexample@email.com\u003e"
				}
			}]
		}]
	},
	"uptime": null,
	"versionInfo": {
		"branch": "N/A",
		"buildDate": "N/A",
		"buildUser": "N/A",
		"goVersion": "N/A",
		"revision": "N/A",
		"version": "N/A"
	}
}
`
	cfgWithoutAutogen := fmt.Sprintf(cfgTemplate, `{
			"receiver": "grafana-default-email",
			"group_by": ["grafana_folder", "alertname"]
		}`)
	cfgWithAutogen := fmt.Sprintf(cfgTemplate, `{
					"receiver": "grafana-default-email",
					"routes": [{
						"receiver": "grafana-default-email",
						"object_matchers": [["__grafana_autogenerated__", "=", "true"]],
						"routes": [{
							"receiver": "grafana-default-email",
							"group_by": ["grafana_folder", "alertname"],
							"object_matchers": [["__grafana_receiver__", "=", "grafana-default-email"]]
						}]
					}],
					"group_by": ["grafana_folder", "alertname"]
				}`)

	testCases := []testCase{
		{
			desc:      "un-authenticated request should fail",
			url:       "http://%s/api/alertmanager/grafana/api/v2/status",
			expStatus: http.StatusUnauthorized,
			expBody:   `{"extra":null,"message":"Unauthorized","messageId":"auth.unauthorized","statusCode":401,"traceID":""}`,
		},
		{
			desc:      "viewer request should succeed",
			url:       "http://viewer:viewer@%s/api/alertmanager/grafana/api/v2/status",
			expStatus: http.StatusOK,
			expBody:   cfgWithoutAutogen,
		},
		{
			desc:      "editor request should succeed",
			url:       "http://editor:editor@%s/api/alertmanager/grafana/api/v2/status",
			expStatus: http.StatusOK,
			expBody:   cfgWithoutAutogen,
		},
		{
			desc:      "admin request should succeed",
			url:       "http://admin:admin@%s/api/alertmanager/grafana/api/v2/status",
			expStatus: http.StatusOK,
			expBody:   cfgWithAutogen,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.desc, func(t *testing.T) {
			resp, err := http.Get(fmt.Sprintf(tc.url, grafanaListedAddr))
			t.Cleanup(func() {
				require.NoError(t, resp.Body.Close())
			})
			require.NoError(t, err)
			require.Equal(t, tc.expStatus, resp.StatusCode)
			b, err := io.ReadAll(resp.Body)
			if tc.expStatus == http.StatusOK {
				re := regexp.MustCompile(`"uid":"([\w|-]+)"`)
				b = re.ReplaceAll(b, []byte(`"uid":""`))
			}
			require.NoError(t, err)
			require.JSONEq(t, tc.expBody, string(b))
		})
	}
}
